--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit c71ae17535d1c6b9712c910689c022d98afa0c13
Parents : 7fd826a
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-15T03:11:13-05:00
feat(licenses): add functionality to collect and write frontend licenses and third-party notices; implement embedded data paths and update build process to include license artifacts
Changes
4 files changed, 238 insertions(+), 13 deletions(-)
Diff
diff --git a/cx_setup.py b/cx_setup.py
index 76aea6e8..ad59ac34 100644
--- a/cx_setup.py
+++ b/cx_setup.py
@@ -18,6 +18,18 @@ changelog_path = ROOT / "CHANGELOG.md"
if changelog_path.exists():
include_files.append((str(changelog_path), "CHANGELOG.md"))
+frontend_licenses_path = (
+ ROOT / "meshchatx" / "src" / "backend" / "data" / "licenses_frontend.json"
+)
+if frontend_licenses_path.exists():
+ include_files.append((str(frontend_licenses_path), "licenses_frontend.json"))
+
+third_party_notices_path = (
+ ROOT / "meshchatx" / "src" / "backend" / "data" / "THIRD_PARTY_NOTICES.txt"
+)
+if third_party_notices_path.exists():
+ include_files.append((str(third_party_notices_path), "THIRD_PARTY_NOTICES.txt"))
+
if PUBLIC_DIR.exists() and PUBLIC_DIR.is_dir():
include_files.append((str(PUBLIC_DIR), "public"))
diff --git a/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt b/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt
new file mode 100644
index 00000000..61d1beb8
--- /dev/null
+++ b/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt
@@ -0,0 +1,91 @@
+Reticulum MeshChatX - Third-party notices
+Generated at: 2026-04-15T07:29:13.270941Z
+Frontend source: embedded
+
+Python dependencies
+-------------------
+aiohappyeyeballs 2.6.1
+ License: PSF-2.0
+ Author: J. Nick Koston
+aiohttp 3.13.5
+ License: Apache-2.0 AND MIT
+ Author: —
+aiohttp-session 2.12.1
+ License: Apache 2
+ Author: Andrew Svetlov
+aiosignal 1.4.0
+ License: Apache 2.0
+ Author: aiohttp team <team@aiohttp.org>
+attrs 26.1.0
+ License: MIT
+ Author: Hynek Schlawack <hs@ox.cx>
+audioop-lts 0.2.2
+ License: PSF-2.0
+ Author: —
+bcrypt 5.0.0
+ License: Apache-2.0
+ Author: The Python Cryptographic Authority developers <cryptography-dev@python.org>
+cffi 2.0.0
+ License: MIT
+ Author: Armin Rigo, Maciej Fijalkowski
+cryptography 46.0.7
+ License: Apache-2.0 OR BSD-3-Clause
+ Author: The Python Cryptographic Authority and individual contributors <cryptography-dev@python.org>
+frozenlist 1.8.0
+ License: Apache-2.0
+ Author: aiohttp team <team@aiohttp.org>
+idna 3.11
+ License: BSD-3-Clause
+ Author: Kim Davies <kim+pypi@gumleaf.org>
+jaraco.context 6.1.2
+ License: MIT
+ Author: "Jason R. Coombs" <jaraco@jaraco.com>
+lxmf 0.9.4
+ License: Reticulum License
+ Author: Mark Qvist
+lxmfy 1.6.1
+ License: BSD-0-Clause
+ Author: Quad4
+lxst 0.4.6
+ License: Other/Proprietary License
+ Author: Mark Qvist
+multidict 6.7.1
+ License: Apache License 2.0
+ Author: Andrew Svetlov
+numpy 2.4.4
+ License: BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0
+ Author: Travis E. Oliphant et al.
+ply 3.11
+ License: BSD
+ Author: David Beazley
+propcache 0.4.1
+ License: Apache-2.0
+ Author: Andrew Svetlov
+psutil 7.2.2
+ License: BSD-3-Clause
+ Author: Giampaolo Rodola
+pycodec2 4.1.1
+ License: OSI Approved :: BSD License
+ Author: Grzegorz Milka
+pycparser 3.0
+ License: BSD-3-Clause
+ Author: Eli Bendersky <eliben@gmail.com>
+pyserial 3.5
+ License: BSD
+ Author: Chris Liechti
+reticulum-meshchatx 4.4.0
+ License: MIT
+ Author: Sudo-Ivan
+rns 1.1.5
+ License: Reticulum License
+ Author: Mark Qvist
+websockets 16.0
+ License: BSD-3-Clause
+ Author: Aymeric Augustin <aymeric.augustin@m4x.org>
+yarl 1.23.0
+ License: Apache-2.0
+ Author: Andrew Svetlov
+
+Node dependencies
+-----------------
+No entries.
diff --git a/meshchatx/src/backend/licenses_collector.py b/meshchatx/src/backend/licenses_collector.py
index b2acab13..e084fc37 100644
--- a/meshchatx/src/backend/licenses_collector.py
+++ b/meshchatx/src/backend/licenses_collector.py
@@ -6,6 +6,7 @@ import importlib.metadata
import json
import shutil
import subprocess
+import sys
import tomllib
from datetime import UTC, datetime
from pathlib import Path
@@ -15,6 +16,9 @@ from packaging.requirements import Requirement
from packaging.utils import canonicalize_name
_ROOT_DIST_CANDIDATES = ("reticulum-meshchatx", "reticulum_meshchatx")
+_DATA_SUBPATH = Path("meshchatx") / "src" / "backend" / "data"
+_FRONTEND_LICENSES_FILENAME = "licenses_frontend.json"
+_THIRD_PARTY_NOTICES_FILENAME = "THIRD_PARTY_NOTICES.txt"
def _repo_root() -> Path:
@@ -162,18 +166,35 @@ def _flatten_pnpm_licenses_json(data: dict[str, Any]) -> list[dict[str, Any]]:
return out
+def _embedded_data_paths(filename: str) -> list[Path]:
+ paths = [Path(__file__).resolve().parent / "data" / filename]
+ exe_parent = Path(sys.executable).resolve().parent
+ paths.append(exe_parent / filename)
+ paths.append(exe_parent / "data" / filename)
+ paths.append(exe_parent / _DATA_SUBPATH / filename)
+ seen: set[str] = set()
+ unique_paths: list[Path] = []
+ for path in paths:
+ key = str(path)
+ if key in seen:
+ continue
+ seen.add(key)
+ unique_paths.append(path)
+ return unique_paths
+
+
def _load_embedded_frontend_licenses() -> list[dict[str, Any]] | None:
- data_dir = Path(__file__).resolve().parent / "data"
- path = data_dir / "licenses_frontend.json"
- if not path.is_file():
- return None
- try:
- raw = json.loads(path.read_text(encoding="utf-8"))
- except (OSError, json.JSONDecodeError):
- return None
- if not isinstance(raw, list):
- return None
- return [x for x in raw if isinstance(x, dict)]
+ for path in _embedded_data_paths(_FRONTEND_LICENSES_FILENAME):
+ if not path.is_file():
+ continue
+ try:
+ raw = json.loads(path.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError):
+ continue
+ if not isinstance(raw, list):
+ continue
+ return [x for x in raw if isinstance(x, dict)]
+ return None
def collect_frontend_licenses() -> tuple[list[dict[str, Any]], str]:
@@ -221,3 +242,82 @@ def build_licenses_payload() -> dict[str, Any]:
"frontend_source": fe_source,
},
}
+
+
+def render_third_party_notices(payload: dict[str, Any]) -> str:
+ """Render third-party dependency notices as plain text."""
+ meta = payload.get("meta", {}) if isinstance(payload, dict) else {}
+ generated_at = str(meta.get("generated_at", "unknown"))
+ frontend_source = str(meta.get("frontend_source", "unknown"))
+ lines = [
+ "Reticulum MeshChatX - Third-party notices",
+ f"Generated at: {generated_at}",
+ f"Frontend source: {frontend_source}",
+ "",
+ ]
+ sections: list[tuple[str, list[dict[str, Any]]]] = [
+ ("Python dependencies", payload.get("backend", [])),
+ ("Node dependencies", payload.get("frontend", [])),
+ ]
+ for title, rows in sections:
+ lines.append(title)
+ lines.append("-" * len(title))
+ if not isinstance(rows, list) or not rows:
+ lines.append("No entries.")
+ lines.append("")
+ continue
+ for row in rows:
+ if not isinstance(row, dict):
+ continue
+ name = str(row.get("name", "?"))
+ version = str(row.get("version", "?"))
+ author = str(row.get("author", "—"))
+ license_name = str(row.get("license", "—"))
+ lines.append(f"{name} {version}")
+ lines.append(f" License: {license_name}")
+ lines.append(f" Author: {author}")
+ lines.append("")
+ return "\n".join(lines).rstrip() + "\n"
+
+
+def write_embedded_license_artifacts(repo_root: Path | None = None) -> dict[str, Any]:
+ """Generate and write embedded license metadata and notices artifacts."""
+ if repo_root is None:
+ repo_root = _repo_root()
+ data_dir = repo_root / _DATA_SUBPATH
+ data_dir.mkdir(parents=True, exist_ok=True)
+ payload = build_licenses_payload()
+ frontend = payload.get("frontend", [])
+ meta = payload.get("meta", {}) if isinstance(payload, dict) else {}
+ frontend_source = str(meta.get("frontend_source", "unknown"))
+ frontend_path = data_dir / _FRONTEND_LICENSES_FILENAME
+ notices_path = data_dir / _THIRD_PARTY_NOTICES_FILENAME
+ frontend_rows = frontend if isinstance(frontend, list) else []
+ should_write_frontend = (
+ bool(frontend_rows) or not frontend_path.exists() or frontend_source == "pnpm"
+ )
+ if should_write_frontend:
+ frontend_path.write_text(
+ json.dumps(frontend_rows, indent=2, ensure_ascii=False) + "\n",
+ encoding="utf-8",
+ )
+ notices_path.write_text(render_third_party_notices(payload), encoding="utf-8")
+ return {
+ "frontend_path": str(frontend_path),
+ "frontend_count": len(frontend_rows),
+ "frontend_written": should_write_frontend,
+ "notices_path": str(notices_path),
+ }
+
+
+def main() -> int:
+ if len(sys.argv) > 1 and sys.argv[1] == "--write-artifacts":
+ result = write_embedded_license_artifacts()
+ print(json.dumps(result, indent=2))
+ return 0
+ print(json.dumps(build_licenses_payload(), indent=2))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/build-backend.js b/scripts/build-backend.js
index 3f9f3dbc..5bd7b832 100755
--- a/scripts/build-backend.js
+++ b/scripts/build-backend.js
@@ -91,11 +91,33 @@ try {
const cmdParts = pythonCmd.trim().split(/\s+/).filter(Boolean);
const cmd = cmdParts[0];
- const args = [...cmdParts.slice(1), "cx_setup.py", "build"];
+ const baseArgs = cmdParts.slice(1);
+ const licensesArgs = [...baseArgs, "-m", "meshchatx.src.backend.licenses_collector", "--write-artifacts"];
+ const args = [...baseArgs, "cx_setup.py", "build"];
let spawnCmd = cmd;
- let spawnArgs = args;
+ let spawnArgs = licensesArgs;
const rosettaX64 = isDarwin && arch === "x64" && process.arch === "arm64" && !process.env.PYTHON_CMD;
+ if (rosettaX64) {
+ spawnCmd = "arch";
+ spawnArgs = ["-x86_64", cmd, ...licensesArgs];
+ }
+
+ console.log("Generating embedded third-party license artifacts...");
+ const licensesResult = spawnSync(spawnCmd, spawnArgs, {
+ stdio: "inherit",
+ shell: false,
+ env: env,
+ });
+ if (licensesResult.error) {
+ throw licensesResult.error;
+ }
+ if (licensesResult.status !== 0) {
+ process.exit(licensesResult.status || 1);
+ }
+
+ spawnCmd = cmd;
+ spawnArgs = args;
if (rosettaX64) {
spawnCmd = "arch";
spawnArgs = ["-x86_64", cmd, ...args];
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────